﻿namespace Home.Test.WeiXin
{
    using Home.Test.Repository;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Threading.Tasks;
    using System.Web;
    using System.Globalization;

    public class ConcreteIdentityProviderHandler : BaseIdentityProviderHandler
    {
        #region Request distribution

        protected override RequestType DetermineRequestType(HttpContext context)
        {
            var result = RequestType.Unknown;

            var segments = context.Request.Url.Segments;
            if (segments.Length == 2 && segments[0] == "weixin")
            {
                switch (segments[1])
                {
                    case "authorize":
                        {
                            result = RequestType.Authorize;
                            break;
                        }
                    case "accesstoken":
                        {
                            result = RequestType.AccessToken;
                            break;
                        }
                    case "refreshtoken":
                        {
                            result = RequestType.RefreshToken;
                            break;
                        }
                    case "checktoken":
                        {
                            result = RequestType.ValidateToken;
                            break;
                        }
                    case "userinfo":
                        {
                            result = RequestType.UserInfo;
                            break;
                        }
                }
            }

            return result;
        }

        protected async override Task ProcessRequestAsync(HttpContext context, RequestType type)
        {
            switch (type)
            {
                case RequestType.Authorize:
                    {
                        await ProcessAuthorizeAsync(context);
                        break;
                    }
                case RequestType.AccessToken:
                    {
                        await ProcessAccessTokenAsync(context);
                        break;
                    }
                case RequestType.RefreshToken:
                    {
                        await ProcessRefreshTokenAsync(context);
                        break;
                    }
                case RequestType.ValidateToken:
                    {
                        await ProcessValidateTokenAsync(context);
                        break;
                    }
                case RequestType.UserInfo:
                    {
                        await ProcessUserInfoAsync(context);
                        break;
                    }
            }
        }

        #endregion

        #region Process authorize

        private static async Task ProcessAuthorizeAsync(HttpContext context)
        {
            var request = context.Request;
            var response = context.Response;

            var queryString = new AuthorizationQueryString()
            {
                AppId = request.QueryString["appid"],
                ResponseType = request.QueryString["response_type"],
                RedirectUri = request.QueryString["redirect_uri"],
                Scope = request.QueryString["scope"],
                State = request.QueryString["state"],
            };

            // validate request

            if (queryString.ResponseType != "code"
                || queryString.Scope != "snsapi_login,snsapi_base,snsapi_userinfo"
                || !request.RawUrl.EndsWith("#wechat_redirect", StringComparison.InvariantCulture)
                || string.IsNullOrEmpty(queryString.RedirectUri)
                || string.IsNullOrEmpty(queryString.AppId))
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var client = await GetClientAsync(queryString.AppId);
            if (client == null || new Uri(queryString.RedirectUri, UriKind.Absolute) != new Uri(client.CallbackUrl, UriKind.Absolute))
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            // redirect user to login page if the user is not logged in

            var cookie = default(Cookie);
            if (!Helper.Authentication.TryGetSignInData(context, IdentityProviderSource.WeiXin, out cookie))
            {
                var param = Helper.Crypto.Protect(queryString);
                var redirectUrl = Helper.Web.ToUrl("weixin/login", new { param = param });
                redirectUrl = Helper.Web.ToAbsolute(context, redirectUrl);
                ResponseRedirect(context, redirectUrl);
                return;
            }

            // generate authorize code

            var consent = await GetConsentAsync(cookie.ConsentId);
            if (consent == null)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var code = Helper.Crypto.Protect(new Token()
            {
                Expires = DateTime.UtcNow.AddSeconds(Helper.WeiXin.DefaultExpiresDuration.AuthorizationCode),
                Scope = queryString.Scope,
                OpenId = consent.OpenId,
                ConsentId = cookie.ConsentId,
            });

            // response

            var callback = default(string);
            if (queryString.State == null)
            {
                callback = Helper.Web.ToUrl(client.CallbackUrl, new { code = code });
            }
            else
            {
                callback = Helper.Web.ToUrl(client.CallbackUrl, new { code = code, state = queryString.State });
            }
            ResponseRedirect(context, callback);
        }

        #endregion

        #region Process access token

        private static async Task ProcessAccessTokenAsync(HttpContext context)
        {
            var request = context.Request;
            var response = context.Response;

            var queryString = new
            {
                AppId = request.QueryString["appid"],
                Secret = request.QueryString["secret"],
                Code = request.QueryString["code"],
                GrantType = request.QueryString["grant_type"],
            };

            // validate request

            if (queryString.GrantType != "authorization_code"
                || string.IsNullOrEmpty(queryString.AppId)
                || string.IsNullOrEmpty(queryString.Secret)
                || string.IsNullOrEmpty(queryString.Code))
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var client = await GetClientAsync(queryString.AppId);
            if (client == null || client.Secret != queryString.Secret)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var code = Helper.Crypto.Unprotect<Token>(queryString.Code);
            if (code == null || code.Expires <= DateTime.UtcNow)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            // generate access token and refresh token

            var publishing = DateTime.UtcNow;
            var accessToken = new Token()
            {
                Scope = code.Scope,
                OpenId = code.OpenId,
                ConsentId = code.ConsentId,
                Expires = publishing.AddSeconds(Helper.WeiXin.DefaultExpiresDuration.AccessToken),
            };
            var refreshToken = new Token()
            {
                Scope = code.Scope,
                OpenId = code.OpenId,
                ConsentId = code.ConsentId,
                Expires = publishing.AddSeconds(Helper.WeiXin.DefaultExpiresDuration.RefreshToken),
            };

            // response

            var content = new JObject();
            content["access_token"] = Helper.Crypto.Protect(accessToken);
            content["refresh_token"] = Helper.Crypto.Protect(refreshToken);
            content["expires_in"] = Helper.WeiXin.DefaultExpiresDuration.AccessToken;
            content["openid"] = code.OpenId;
            content["scope"] = code.Scope;
            await ResponseJsonAsync(context, content.ToString(Formatting.None));
        }

        #endregion

        #region Process refresh token

        private static async Task ProcessRefreshTokenAsync(HttpContext context)
        {
            var request = context.Request;
            var response = context.Response;

            var queryString = new
            {
                AppId = request.QueryString["appid"],
                RefreshToken = request.QueryString["refresh_token"],
                GrantType = request.QueryString["grant_type"],
            };

            // validate request

            if (queryString.GrantType != "refresh_token"
                || string.IsNullOrEmpty(queryString.AppId)
                || string.IsNullOrEmpty(queryString.RefreshToken))
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var client = await GetClientAsync(queryString.AppId);
            if (client == null)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var refreshToken = Helper.Crypto.Unprotect<Token>(queryString.RefreshToken);
            if (refreshToken == null || refreshToken.Expires <= DateTime.UtcNow)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            // generate access token

            var accessToken = new Token()
            {
                Scope = refreshToken.Scope,
                OpenId = refreshToken.OpenId,
                ConsentId = refreshToken.ConsentId,
                Expires = DateTime.UtcNow.AddSeconds(Helper.WeiXin.DefaultExpiresDuration.AccessToken),
            };

            // response

            var content = new JObject();
            content["access_token"] = Helper.Crypto.Protect(accessToken);
            content["refresh_token"] = Helper.Crypto.Protect(refreshToken);
            content["expires_in"] = Helper.WeiXin.DefaultExpiresDuration.AccessToken;
            content["openid"] = accessToken.OpenId;
            content["scope"] = accessToken.Scope;
            await ResponseJsonAsync(context, content.ToString(Formatting.None));
        }

        #endregion

        #region Process validation

        private static async Task ProcessValidateTokenAsync(HttpContext context)
        {
            var request = context.Request;
            var response = context.Response;

            var queryString = new
            {
                AccessToken = request.QueryString["access_token"],
                OpenId = request.QueryString["openid"],
            };

            // validate request

            if (string.IsNullOrEmpty(queryString.AccessToken) || string.IsNullOrEmpty(queryString.OpenId))
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var accessToken = Helper.Crypto.Unprotect<Token>(queryString.AccessToken);
            if (accessToken == null || accessToken.Expires <= DateTime.UtcNow || accessToken.OpenId != queryString.OpenId)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            // response

            await ResponseErrorAsync(context, Helper.IdentityProvider.CodeOk);
        }

        #endregion

        #region Process user info

        private static async Task ProcessUserInfoAsync(HttpContext context)
        {
            var request = context.Request;
            var response = context.Response;

            var queryString = new
            {
                AccessToken = request.QueryString["access_token"],
                OpenId = request.QueryString["openid"],
            };

            // validate request

            if (string.IsNullOrEmpty(queryString.AccessToken) || string.IsNullOrEmpty(queryString.OpenId))
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var accessToken = Helper.Crypto.Unprotect<Token>(queryString.AccessToken);
            if (accessToken == null || accessToken.Expires <= DateTime.UtcNow || accessToken.OpenId != queryString.OpenId)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            // response

            var consent = await GetConsentAsync(accessToken.ConsentId);
            if (consent == null || consent.OpenId != queryString.OpenId)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var sns = await GetSnsAsync(consent.SnsId);
            if (sns == null)
            {
                await ResponseErrorAsync(context, Helper.IdentityProvider.CodeError);
                return;
            }

            var content = new JObject();
            content["id"] = Helper.ToCodeString(sns.Id);
            content["nickname"] = sns.Nickname;
            content["openid"] = consent.OpenId;
            await ResponseJsonAsync(context, content.ToString(Formatting.None));
        }

        #endregion

        #region others

        private static async Task ResponseErrorAsync(HttpContext context, int error)
        {
            var response = context.Response;
            response.Clear();
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = Helper.ContentType.Json;
            var message = new JObject();
            message["errcode"] = error;
            message["errmsg"] = Helper.IdentityProvider.Errors[error];
            await response.Output.WriteLineAsync(message.ToString(Formatting.None));
            await response.Output.FlushAsync();
        }

        #endregion
    }
}